iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Kubernetes

不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA系列 第 12

Day 12|微服務開始成形:加入 Redis,第一次讓 Pod 透過 Service 找 Pod

  • 分享至 

  • xImage
  •  

前面我們已經成功把 FastAPI 部署到 Kubernetes,現在整個系統還非常單純:

Client
  ↓
API

使用者送出 Request,Kubernetes 的 Service 會把流量送到 FastAPI Pod,FastAPI 處理完之後直接回傳 Response。

不過真正的後端系統通常不會只有一個 API。Application 往往還需要連線資料庫、快取服務、Message Queue,甚至其他 Microservice。

所以今天我們要跨出非常重要的一步:

讓兩個 Kubernetes Service 彼此溝通。

今天我們會加入 Redis,並讓 FastAPI 每收到一次 Request,就把 Redis 裡的 visits 數字加一。

最後整個架構會變成:

Client
  ↓
Service: api
  ↓
API Pod
  ↓
Service: redis
  ↓
Redis Pod

這看起來只是多了一個 Redis,但其實今天會碰到 Kubernetes 裡非常重要的觀念:

Service Discovery 與 Kubernetes DNS。


今天要完成什麼?

我們希望使用者呼叫 FastAPI:

GET /

第一次可能回傳:

{
  "message": "Hello from Kubernetes",
  "visits": 1
}

再呼叫一次:

{
  "message": "Hello from Kubernetes",
  "visits": 2
}

這個 visits 並不是存在 FastAPI 自己的記憶體裡,而是存在 Redis。

也就是說,每次 Request 發生時,FastAPI 都必須先連到另一個 Pod:

FastAPI Pod
    ↓
Redis Pod

但這裡馬上出現一個問題。

FastAPI 要怎麼知道 Redis Pod 在哪裡?

這就是今天最重要的地方。


修改 requirements.txt

FastAPI 原本沒有 Redis Client,所以第一步先安裝 Python 的 Redis 套件。

修改:

app/requirements.txt

加入:

redis

之後重新 Build Docker Image 時,Docker 就會把這個套件一起安裝進新的 API Image。


修改 FastAPI

接著修改:

app/main.py

內容變成:

import os

import redis

from fastapi import FastAPI


app = FastAPI()


REDIS_HOST = os.getenv(
    "REDIS_HOST",
    "redis"
)


redis_client = redis.Redis(
    host=REDIS_HOST,
    port=6379,
    decode_responses=True
)


@app.get("/")
def root():

    visits = redis_client.incr("visits")

    return {
        "message": "Hello from Kubernetes",
        "visits": visits
    }


@app.get("/health/live")
def live():

    return {
        "status": "alive"
    }

這段程式碼最重要的不是 incr(),而是這一段:

REDIS_HOST = os.getenv(
    "REDIS_HOST",
    "redis"
)

它的意思是:

如果環境變數裡有:

REDIS_HOST

就使用環境變數提供的值。

如果沒有設定,就預設使用:

redis

接著 Redis Client 使用:

redis.Redis(
    host=REDIS_HOST,
    port=6379
)

也就是 FastAPI 會嘗試連線:

redis:6379

看到這裡你可能會覺得奇怪。

redis 又不是 IP Address,電腦怎麼知道它在哪裡?

這就是 Kubernetes Service 發揮作用的地方。


Kubernetes 裡不要直接連 Pod IP

假設 Redis Pod 現在的 IP 是:

10.244.0.12

我們當然可以讓 FastAPI 直接連:

10.244.0.12:6379

但這是一個非常不好的做法。

原因是 Pod 本身是暫時性的。

如果 Redis Pod Crash:

Redis Pod A
10.244.0.12

Kubernetes 重新建立一個新的 Pod,新的 Pod 很可能變成:

Redis Pod B
10.244.0.25

如果 FastAPI 寫死:

10.244.0.12

那新的 Redis Pod 建立之後,FastAPI 還是會繼續連舊 IP,Application 就會壞掉。

所以 Kubernetes 不希望 Application 直接依賴 Pod IP。

正確做法是:

FastAPI
↓
Redis Service
↓
Redis Pod

FastAPI 只需要知道 Service 的名稱:

redis

至於後面的 Redis Pod IP 是什麼,交給 Kubernetes 處理。


建立 Redis Deployment 與 Service

建立:

k8s/04-redis.yaml

內容如下:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: redis             # Deployment 名稱
  namespace: cka-lab      # 部署在 cka-lab 命名空間下

spec:                     # 【外層 spec】:Deployment 的運作規則
  replicas: 1             # Pod 的期望副本數為 1。死掉會自動重生,維持有 1 個在跑

  selector:               # Deployment 用來認養 Pod 的條件
    matchLabels:
      app: redis

  template:               # 【Pod 模板】:所有被建立出來的 Pod 都長這樣
    metadata:
      labels:             # 給每個產出的 Pod 貼上的標籤
        app: redis

    spec:                 # 【內層 spec】:Pod 裡面運行容器的具體設定
      containers:
        - name: redis                  # 容器名稱
          image: redis:7-alpine        # Docker 映像檔版本
          ports:
            - containerPort: 6379      # 容器內部監聽的 Port

---

apiVersion: v1
kind: Service

metadata:
  name: redis             # Service 名稱(同 namespace 內可直接透過 "redis" 連線)
  namespace: cka-lab

spec:                     # 【Service 的 spec】
  selector:
    app: redis            # 將流量導向所有帶有 app: redis 標籤的 Pod

  ports:
    - port: 6379          # Service 自己對外暴露的 Port
      targetPort: 6379    # 轉發進後端 Pod 容器的目標 Port

https://ithelp.ithome.com.tw/upload/images/20260910/20168537GlStUr7ROg.png

這個 YAML 其實建立了兩個不同的 Kubernetes Resource:

Deployment
+
Service

Deployment 負責:

建立與管理 Redis Pod

Service 則負責:

提供一個穩定的存取入口

因此架構實際上是:

Redis Service
     ↓
 Redis Pod

Service 裡:

selector:
  app: redis

會去尋找具有:

labels:
  app: redis

的 Pod。

而 Redis Deployment 建立的 Pod 剛好就有:

labels:
  app: redis

因此 Service 就知道:

我要把流量送給哪些 Pod。

port 與 targetPort 到底差在哪?

Redis Service 裡有:

ports:
  - port: 6379
    targetPort: 6379

可以簡單理解成:

Service Port
6379
↓
Pod Port
6379

port 是 Service 對外提供的 Port。

targetPort 則是 Service 最後要把流量送到 Pod 的哪一個 Port。

因為 Redis 本身預設就是監聽:

6379

所以這裡兩個數字剛好一樣。

因此 FastAPI:

redis:6379

實際流量就會變成:

FastAPI Pod
    ↓
redis:6379
    ↓
抵達 Redis Service
    ↓
Redis Pod:6379

為什麼 redis 這個名稱可以直接使用?

這是 Kubernetes 很重要的一個功能:

內建 DNS。

當我們建立:

kind: Service

metadata:
  name: redis
  namespace: cka-lab

Kubernetes 會讓 Cluster 內的 Pod 可以透過 Service Name 找到它。

因此同一個 Namespace 裡的 FastAPI 可以直接使用:

redis

Kubernetes DNS 會幫我們解析成 Redis Service。

完整的 DNS Name 實際上大致是:

redis.cka-lab.svc.cluster.local

它可以拆成:

redis
↓
Service Name

cka-lab
↓
Namespace

svc
↓
Service

cluster.local
↓
Cluster Domain

所以:

redis

其實只是同 Namespace 情況下的簡寫。

也就是說,FastAPI 根本不需要知道 Redis Pod IP。

只要知道:

Service Name = redis

就夠了。

這就是 Kubernetes 的 Service Discovery


重新 Build API Image

因為我們剛剛修改了:

requirements.txt

以及:

main.py

這些都屬於 Application 程式碼與 Dependency 的改動,所以原本的 Docker Image 已經不包含最新內容。

因此需要重新 Build。

建立:

docker build -t cka-api:v3 ./app

現在本機就會多出:

cka-api:v3

因為我們使用的是 kind,而 kind Node 本身其實跑在 Docker Container 裡,所以本機 Docker 裡的 Image 不代表 kind Cluster 一定看得到。

因此還需要:

kind load docker-image cka-api:v3 --name cka-lab

把 Image 放進 kind Cluster。

https://ithelp.ithome.com.tw/upload/images/20260910/201685372jVQcas5iP.png

更新 API Deployment

接著把 API Deployment 使用的 Image 修改成:

image: cka-api:v3

例如:

containers:
  - name: api
    image: cka-api:v3

最後重新套用 Kubernetes YAML:

kubectl apply -f k8s/

https://ithelp.ithome.com.tw/upload/images/20260910/20168537nveZEpnhir.png!

Kubernetes 會發現:

原本 API Deployment
cka-api:v2

已經變成:

cka-api:v3

Deployment 的 Pod Template 發生變化,因此 Kubernetes 會自動進行 Rolling Update,建立新的 Pod,逐步替換舊 Pod。

可以查看:

kubectl get pods -n cka-lab

也可以查看 Deployment:

kubectl rollout status \
  deployment/api \
  -n cka-lab

https://ithelp.ithome.com.tw/upload/images/20260910/20168537vSfInCY4ob.png

如果一切正常,新的 API Pod 就會開始使用:

cka-api:v3

9/10

測試完整架構

接著把 API Service 暫時 Forward 到 Mac。

執行:

kubectl port-forward service/api 8080:80 -n cka-lab

現在:

Mac localhost:8080

會被轉送到:

API Service:80

另外開一個 Terminal:

curl localhost:8080

第一次可能看到:

{
  "message": "Hello from Kubernetes",
  "visits": 1
}

https://ithelp.ithome.com.tw/upload/images/20260910/20168537zb7G4k26nX.png!

再執行一次:

curl localhost:8080

變成:

{
  "message": "Hello from Kubernetes",
  "visits": 2
}

https://ithelp.ithome.com.tw/upload/images/20260910/20168537ZV7szH8aSC.png

如果數字真的持續增加,就代表我們已經成功完成:

Mac
 ↓
API Service
 ↓
API Pod
 ↓
Redis Service
 ↓
Redis Pod

這跟前幾天只有:

Client
↓
API Service
↓
API Pod

相比已經往真正的 Microservice Architecture 跨出很大一步。

因為現在已經不是單一 Application 自己運作,而是:

一個 Service
需要依賴另一個 Service。

自己驗證 Kubernetes DNS

既然我們說:

redis

可以被 Kubernetes DNS 解析,那當然可以自己驗證看看。

首先可以進入 API Pod:

kubectl exec -it deployment/api -n cka-lab -- sh

進去之後嘗試:

getent hosts redis

https://ithelp.ithome.com.tw/upload/images/20260910/20168537fuQdFAHfJ2.png

如果這個 Container Image 有安裝對應工具,就可以看到 DNS 解析結果。

不過 Production Image 通常會盡量精簡,因此裡面不一定會放 nslookupdig 這類除錯工具。

這時候更常見的做法,是另外建立一個暫時的 Debug Pod。

例如:

kubectl run dns-test \
  -n cka-lab \
  --image=busybox:1.36 \
  --restart=Never \
  --rm -it \
  -- \
  nslookup redis

這個指令會暫時建立:

dns-test Pod

並在裡面執行:

nslookup redis

你應該可以看到 Kubernetes 把 redis 解析到 Redis Service。

完整名稱會類似:

redis.cka-lab.svc.cluster.local

測試結束之後:

--rm

也會自動把這個 Pod 刪除。

這其實是一個非常實用的 Kubernetes Troubleshooting 技巧。

當 Application 說:

連不到某個 Service

第一件可以確認的事情之一,就是:

DNS 到底有沒有解析成功?

故意製造一次錯誤

現在我們反過來故意把 Application 弄壞。

假設 API Deployment 原本會從 ConfigMap 取得:

REDIS_HOST

原本:

REDIS_HOST=redis

我們故意改成:

REDIS_HOST=not-redis

接著讓 API Pod 重新建立,使新的環境變數載入。

這時候 FastAPI 會嘗試連線:

not-redis:6379

但 Kubernetes 裡根本沒有叫:

not-redis

的 Service。

因此 Application 呼叫 Redis 時就會失敗。

但是這時候如果執行:

kubectl get pods -n cka-lab

你可能還是會看到:

api-xxxxx   1/1   Running

這裡出現了一個非常重要的 Kubernetes 觀念。


Running 不代表 Application 正常

Kubernetes 顯示:

Running

主要代表:

Container Process 還活著。

例如 FastAPI Process 還在執行:

uvicorn

所以 Kubernetes 會認為:

Container 還沒有死。

但使用者實際呼叫:

GET /

FastAPI 又會嘗試:

連線 Redis

Redis Host 寫錯之後,Request 就可能直接失敗。

所以現在出現:

Pod = Running

但是:

Application = 無法正常服務

這兩件事情其實完全可以同時發生。

這也是 Kubernetes 初學者非常容易誤會的地方。

看到:

STATUS = Running

不代表你的服務真的沒有問題。


今天真正學到的是什麼?

今天表面上是在:

安裝 Redis

但真正重要的其實是我們第一次建立了:

Application A
↓
Application B

之間的依賴關係。

FastAPI 不需要知道 Redis Pod 真正的 IP,只需要連:

redis:6379

其中:

redis

是 Kubernetes Service Name。

Kubernetes DNS 負責:

Service Name
↓
Service

而 Service 再透過 Selector 找到:

Pod

因此整個流量實際上是:

FastAPI
   ↓
DNS 查詢 redis
   ↓
Redis Service
   ↓
Service Selector
   ↓
Redis Pod

這就是 Kubernetes Service Discovery 最核心的概念。

而最後我們又發現另一個問題:

Pod Running

並不能保證:

Application Healthy

那 Kubernetes 到底要怎麼知道:

我的 FastAPI 真的還活著嗎?

以及:

它現在到底能不能接收流量?

這就會帶出下一篇非常重要的主題:

Liveness Probe
Readiness Probe

到了這裡,我們的 Kubernetes 專案也正式從:

「把 Container 跑起來」

開始進入:

「如何讓真正的服務穩定運作」

的階段。


上一篇
Day 11|ConfigMap 與 Secret:為什麼設定不能全部寫死在程式?
下一篇
Day 13|加入 PostgreSQL 與 PVC:Pod 可以死,資料不能跟著死
系列文
不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言